<template>
  <div class="gantt-chart" @wheel.passive="wheelHandle">
    <div class="gantt-container" :style="{ height: `calc(100% - ${scrollXBarHeight}px)`,  width: `calc(100% - ${scrollYBarWidth}px)` }">
      <div v-show="!hideHeader" class="gantt-header" :style="{ width: `calc(100% + ${scrollYBarWidth}px)` }">
        <div class="gantt-header-title"
          :style="{ 'line-height': titleHeight + 'px', height: titleHeight + 'px', width: titleWidth + 'px'}">
          <slot name="title"></slot>
        </div>
        <div ref="headerTimeline" class="gantt-header-timeline">
          <div class="gantt-timeline-wrapper" :style="{ width: totalWidth + scrollYBarWidth + 'px' }">
            <time-line :start="start" :end="end" :cellWidth="cellWidth" :titleHeight="titleHeight" :scale="scale"></time-line>
          </div>
        </div>
      </div>
      
      <div class="gantt-body" :style="{ height: `calc(100% - ${actualHeaderHeight}px)` }">
        <div class="gantt-table">
          <div
            ref="marklineArea"
            :style="{ marginLeft: titleWidth + 'px' }"
            class="gantt-markline-area"
          >
            <!--关闭时间标记线-->
            <!--<CurrentTime-->
              <!--v-if="showCurrentTime"-->
              <!--:getPositonOffset="getPositonOffset"-->
            <!--/>-->
            <!--<mark-line-->
              <!--v-for="(times, index) in timeLines"-->
              <!--:key="index"-->
              <!--:markLineTime="times.time"-->
              <!--:getPositonOffset="getPositonOffset"-->
              <!--:color="times.color"-->
            <!--&gt;</mark-line>-->
          </div>
          <div
            ref="leftbarWrapper"
            class="gantt-leftbar-wrapper"
            :style="{
              width: titleWidth + 'px',
              height: `calc(100% + ${scrollXBarHeight}px)`
            }"
          >
            <LeftBar
              :datas="datas"
              :dataKey="dataKey"
              :scrollTop="scrollTop"
              :heightOfBlocksWrapper="heightOfBlocksWrapper"
              :cellHeight="cellHeight"
              :preload="preload"
              :style="{ height: totalHeight + scrollXBarHeight + 'px' }"
            >
              <template v-slot="{ data }">
                <slot name="left" :data="data"> </slot>
              </template>
            </LeftBar>
          </div>
          <div ref="blocksWrapper" class="gantt-blocks-wrapper">
            <blocks
              :scrollTop="scrollTop"
              :scrollLeft="scrollLeft"
              :heightOfBlocksWrapper="heightOfBlocksWrapper"
              :widthOfBlocksWrapper="widthOfBlocksWrapper"
              :arrayKeys="arrayKeys"
              :itemKey="itemKey"
              :dataKey="dataKey"
              :datas="datas"
              :cellWidth="cellWidth"
              :cellHeight="cellHeight"
              :scale="scale"
              :getPositonOffset="getPositonOffset"
              :getWidthAbout2Times="getWidthAbout2Times"
              :customGenerateBlocks="customGenerateBlocks"
              :startTimeOfRenderArea="startTimeOfRenderArea"
              :endTimeOfRenderArea="endTimeOfRenderArea"
              :preload="preload"
              :style="{ width: totalWidth + 'px' }"
            >
              <template v-if="!customGenerateBlocks" v-slot="{ data, item }">
                <slot name="block" :data="data" :item="item"> </slot>
              </template>
              <template
                v-else
                v-slot="{
                  data,
                  getPositonOffset,
                  getWidthAbout2Times,
                  isInRenderingTimeRange,
                  isAcrossRenderingTimeRange }"
              >
                <slot
                  name="block"
                  :data="data"
                  :getPositonOffset="getPositonOffset"
                  :getWidthAbout2Times="getWidthAbout2Times"
                  :isInRenderingTimeRange="isInRenderingTimeRange"
                  :isAcrossRenderingTimeRange="isAcrossRenderingTimeRange"
                  :startTimeOfRenderArea="startTimeOfRenderArea"
                  :endTimeOfRenderArea="endTimeOfRenderArea"
                ></slot>
              </template>
            </blocks>
          </div>
        </div>
      </div>
    </div>
    
    <div
      ref="scrollYBar"
      class="gantt-scroll-y"
      :style="{
        width: `${scrollYBarWidth}px`,
        height: `calc(100% - ${actualHeaderHeight}px`,
        marginTop: `${actualHeaderHeight}px`
      }"
      @scroll.passive="syncScrollY"
    >
      <div :style="{ height: totalHeight + 'px' }"></div>
    </div>
    
    <div
      ref="scrollXBar"
      class="gantt-scroll-x"
      :style="{
        height: `${scrollXBarHeight}px`,
        width: `calc(100% - ${titleWidth}px )`,
        marginLeft: titleWidth + 'px'
      }"
      @scroll.passive="syncScrollX"
    >
      <div :style="{ width: totalWidth + 'px' }"></div>
    </div>
  </div>
</template>

<script>
  import DayJs from "dayjs";
  import ResizeObserver from "resize-observer-polyfill";
  import {scaleList, getBeginTimeOfTimeLine, calcScalesAbout2Times} from "@/utils/timeLineUtils";
  // import { isDef, warn } from "./utils/tool.js";
  import * as Utils from '@/utils/index'
  import {
    getPositonOffset as _getPositonOffset,
    getWidthAbout2Times as _getWidthAbout2Times
  } from "@/utils/gtUtils";
  import throttle from "@/utils/throttle";
  import TimeLine from "@/components/gantt/time-line/index";
  import CurrentTime from "@/components/gantt/mark-line/current-time";
  import LeftBar from "@/components/gantt/left-bar/index";
  import Blocks from "@/components/gantt/blocks/index";
  import MarkLine from "@/components/gantt/mark-line/index";
  export default {
    name: "Gantt",
    components: { TimeLine, LeftBar, Blocks, MarkLine, CurrentTime },
    props: {
      startTime: {
        default: () => DayJs(),
        validator(date) {
          const ok = DayJs(date).isValid();
          if (!ok) Utils.warn(`非法的开始时间 ${date}`);
          return ok;
        }
      },
      endTime: {
        default: () => DayJs(),
        validator(date) {
          const ok = DayJs(date).isValid();
          if (!ok) Utils.warn(`非法的结束时间 ${date}`);
          return ok;
        }
      },
      cellWidth: {
        type: Number,
        default: 50
      },
      cellHeight: {
        type: Number,
        default: 20
      },
      titleHeight: {
        type: Number,
        default: 40
      },
      titleWidth: {
        type: Number,
        default: 200
      },
      scale: {
        type: Number,
        default: 60,
        validator(value) {
          return scaleList.includes(value);
        }
      },
      datas: {
        type: Array,
        default: () => []
      },
      dataKey: {
        type: String,
        default: undefined
      },
      itemKey: {
        type: String,
        default: undefined
      },
      arrayKeys: {
        type: Array,
        default: () => []
      },
      showCurrentTime: {
        type: Boolean,
        default: false
      },
      timeLines: {
        type: Array
      },
      scrollToTime: {
        validator(date) {
          return DayJs(date).isValid();
        }
      },
      scrollToPostion: {
        validator(obj) {
          const validX = Utils.isDef(obj.x) ? !Number.isNaN(obj.x) : true;
          const validY = Utils.isDef(obj.y) ? !Number.isNaN(obj.y) : true;
          if (!validX && !validY) {
            Utils.warn("scrollToPostion x或y 有值为非Number类型");
            return false;
          }
          return true;
        }
      },
      hideHeader: {
        type: Boolean,
        default: false
      },
      hideXScrollBar: {
        type: Boolean,
        default: false
      },
      hideYScrollBar: {
        type: Boolean,
        default: false
      },
      customGenerateBlocks: {
        type: Boolean,
        default: false
      },
      timeRangeCorrection: {
        type: Boolean,
        default: true
      },
      preload: {
        type: Number
      }
    },
    data() {
      return {
        //缓存节点
        selector: {
          gantt_leftbar: {},
          gantt_table: {},
          gantt_scroll_y: {},
          gantt_timeline: {},
          gantt_scroll_x: {},
          gantt_markArea: {}
        },
        scrollTop: 0,
        scrollLeft: 0,
        //block 区域需要渲染的范围
        //先渲染出空框架，在mounted后再得到真实的渲染范围，然后在根据范围渲染数据，比之前设置一个默认高度宽度，额外的渲染浪费更少了
        heightOfBlocksWrapper: 0,
        widthOfBlocksWrapper: 0,
        scrollBarWitdh: 17
      };
    },
    computed: {
      start() {
        return DayJs(this.startTime);
      },
      end() {
        const {
          start,
          widthOfBlocksWrapper,
          scale,
          cellWidth,
          timeRangeCorrection
        } = this;
        let end = DayJs(this.endTime);
        const totalWidth = calcScalesAbout2Times(start, end, scale) * cellWidth;
        // 时间纠正和补偿
        if (
          timeRangeCorrection &&
          (start.isAfter(end) || totalWidth <= widthOfBlocksWrapper)
        ) {
          end = start.add((widthOfBlocksWrapper / cellWidth) * scale, "minute");
        }
        return end;
      },
      totalWidth() {
        const { cellWidth, totalScales } = this;
        return cellWidth * totalScales;
      },
      totalScales() {
        const { start, end, scale } = this;
        return calcScalesAbout2Times(start, end, scale);
      },
      totalHeight() {
        const { datas, cellHeight } = this;
        return datas.length * cellHeight;
      },
      beginTimeOfTimeLine() {
        const value = getBeginTimeOfTimeLine(this.start, this.scale);
        return value;
      },
      beginTimeOfTimeLineToString() {
        return this.beginTimeOfTimeLine.toString();
      },
      avialableScrollLeft() {
        // 不减这个1，滚动到时间轴尽头后继续滚动会慢慢的溢出
        const { totalWidth, widthOfBlocksWrapper } = this;
        return totalWidth - widthOfBlocksWrapper - 1;
      },
      avialableScrollTop() {
        const { totalHeight, heightOfBlocksWrapper } = this;
        return totalHeight - heightOfBlocksWrapper - 1;
      },
      scrollXBarHeight() {
        return this.hideXScrollBar ? 0 : this.scrollBarWitdh;
      },
      scrollYBarWidth() {
        return this.hideYScrollBar ? 0 : this.scrollBarWitdh;
      },
      actualHeaderHeight() {
        return this.hideHeader ? 0 : this.titleHeight;
      },
      startTimeOfRenderArea() {
        if (this.heightOfBlocksWrapper === 0) {
          return;
        }
        const { beginTimeOfTimeLine, scrollLeft, cellWidth, scale } = this;
        
        return beginTimeOfTimeLine
          .add((scrollLeft / cellWidth) * scale, "minute")
          .toDate()
          .getTime();
      },
      endTimeOfRenderArea() {
        if (this.heightOfBlocksWrapper === 0) {
          return;
        }
        const {
          beginTimeOfTimeLine,
          scrollLeft,
          cellWidth,
          scale,
          widthOfBlocksWrapper,
          totalWidth
        } = this;
        
        const renderWidth =
          totalWidth < widthOfBlocksWrapper ? totalWidth : widthOfBlocksWrapper;
        
        return beginTimeOfTimeLine
          .add(((scrollLeft + renderWidth) / cellWidth) * scale, "minute")
          .toDate()
          .getTime();
      }
    },
    watch: {
      scrollToTime: {
        handler(newV) {
          if (!newV) {
            return;
          }
          const { start, end } = this;
          const time = DayJs(newV);
          if (!(time.isAfter(start) && time.isBefore(end))) {
            Utils.warn(`当前滚动至${newV}不在${start}和${end}的范围之内`);
            return;
          }
          const offset = this.getPositonOffset(newV);
          this.$nextTick(this.manualScroll(offset));
        },
        immediate: true
      },
      scrollToPostion: {
        handler(newV) {
          if (!newV) {
            return;
          }
          const x = Number.parseFloat(newV.x);
          const y = Number.parseFloat(newV.y);
          if (!Number.isNaN(x) && x !== this.scrollLeft) {
            this.$nextTick(this.manualScroll(x));
          }
          if (!Number.isNaN(y) && y !== this.scrollTop) {
            this.$nextTick(this.manualScroll(undefined, y));
          }
        },
        immediate: true
      }
    },
    mounted() {
      this.cacheSelector();
      // 计算准确的渲染区域范围
      const observeContainer = throttle(entries => {
        entries.forEach(entry => {
          const cr = entry.contentRect;
          this.heightOfBlocksWrapper = cr.height;
          this.widthOfBlocksWrapper = cr.width;
        });
      });
      const observer = new ResizeObserver(observeContainer);
      observer.observe(this.$refs.blocksWrapper);
      this.$once("hook:beforeDestroy", () => {
        observer.disconnect();
        this.releaseSelector();
      });
    },
    methods: {
      getWidthAbout2Times(start, end) {
        const options = {
          scale: this.scale,
          cellWidth: this.cellWidth
        };
        return _getWidthAbout2Times(start, end, options);
      },
      /**
       * 为时间线计算偏移
       */
      getPositonOffset(date) {
        const options = {
          scale: this.scale,
          cellWidth: this.cellWidth
        };
        return _getPositonOffset(date, this.beginTimeOfTimeLineToString, options);
      },
      //缓存节点
      cacheSelector() {
        this.selector.gantt_leftbar = this.$refs.leftbarWrapper;
        this.selector.gantt_table = this.$refs.blocksWrapper;
        this.selector.gantt_scroll_y = this.$refs.scrollYBar;
        this.selector.gantt_timeline = this.$refs.headerTimeline;
        this.selector.gantt_scroll_x = this.$refs.scrollXBar;
        this.selector.gantt_markArea = this.$refs.marklineArea;
      },
      releaseSelector() {
        let key;
        for (key in this.selector) {
          this.selector[key] = null;
        }
      },
      wheelHandle(event) {
        const { deltaX, deltaY } = event;
        const {
          scrollTop,
          scrollLeft,
          avialableScrollLeft,
          avialableScrollTop
        } = this;
        
        if (deltaY !== 0) {
          if (
            scrollTop + deltaY >= avialableScrollTop &&
            scrollTop !== avialableScrollTop
          ) {
            this.manualScroll(undefined, avialableScrollTop);
          } else if (scrollTop + deltaY < 0 && scrollTop !== 0 /*滚动为0限制*/) {
            this.manualScroll(undefined, 0);
          } else {
            this.manualScroll(undefined, scrollTop + deltaY);
          }
        }
        if (deltaX !== 0) {
          if (
            scrollLeft + deltaX >= avialableScrollLeft &&
            scrollLeft !== avialableScrollLeft
          ) {
            this.manualScroll(avialableScrollLeft);
          } else if (
            scrollLeft + deltaX < 0 &&
            scrollLeft !== 0 /*滚动为0限制*/
          ) {
            this.manualScroll(0);
          } else {
            this.manualScroll(scrollLeft + deltaX);
          }
        }
      },
      manualScroll(x, y) {
        if (x !== undefined) {
          this.selector.gantt_scroll_x.scrollLeft = x;
        }
        if (y !== undefined) {
          this.selector.gantt_scroll_y.scrollTop = y;
        }
      },
      //同步fixleft和block的滚动
      syncScrollY(event) {
        const { gantt_leftbar, gantt_table } = this.selector;
        const topValue = event.target.scrollTop;
        this.scrollTop = gantt_table.scrollTop = gantt_leftbar.scrollTop = topValue;
        this.$emit("scrollTop", topValue);
      },
      syncScrollX(event) {
        const { gantt_table, gantt_timeline, gantt_markArea } = this.selector;
        const leftValue = event.target.scrollLeft;
        this.scrollLeft = gantt_timeline.scrollLeft = gantt_table.scrollLeft = leftValue;
        gantt_markArea.style.left = -leftValue + "px";
        this.$emit("scrollLeft", leftValue);
      }
    }
  };
</script>

<style lang="scss">
  @import "./gantt.scss";
</style>
